- 발행일
Django 폼 레이아웃을 도와주는 방법들 — 조사, 그리고 실제로 고른 조합
Django 폼 레이아웃을 도와주는 방법들
Django의 폼(formset 포함)은 데이터 바인딩과 검증까지는 해주지만, 레이아웃과 스타일은 결국 별도의 방법이 필요합니다. 그 방법들을 조사했던 글이고, 뒤에 실제로 고른 조합을 붙였습니다.
날짜 정정 — 이 글은 원래 2022-08-05로 게시돼 있었는데, 본문이 Django 5.2 문서(2025-04 출시)와 django-formify(2024 공개)를 참조하므로 성립할 수 없는 날짜입니다. 같은 시기의 다른 사전 조사 글들과 함께 2025-04로 교정합니다.
공식 문서: ModelForm · django-crispy-forms
1. 후보 비교
| 라이브러리 | 구조 제어 | 스타일 연동 | 주요 특징 |
|---|---|---|---|
| django-crispy-forms | ✅ | Bootstrap / Tailwind | Python 코드로 레이아웃 제어 (Layout, Row, Column) |
| django-formify | ✅ | Tailwind 전용 | 컴포넌트 기반 구조 |
| django-widget-tweaks | ❌ | 자유 | 템플릿에서 필드 속성·클래스 조작 |
| django-bootstrap5 | ❌ | Bootstrap 전용 | 자동 렌더링, 커스터마이징 제한 |
| django-dynamic-formset | ❌ | 자유 | 클라이언트(JS)에서 폼 행 추가/삭제 |
2. 라이브러리 없이 하는 기본기
어느 쪽을 고르든 밑바탕은 같습니다 — 위젯 attrs와 Meta 옵션.
# 위젯 커스터마이징 — 클래스·placeholder·HTML 제약을 서버에서 지정
forms.CharField(
widget=forms.TextInput(attrs={
"class": "input input-bordered",
"placeholder": "이름을 입력하세요",
"maxlength": "30",
})
)
# ModelForm — 모델 기반 자동 생성 + 필드별 위젯 오버라이드
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content']
widgets = {
'content': forms.Textarea(attrs={"class": "w-full p-3 border rounded-md"}),
}
# 조건부 필드 — __init__에서 사용자·상태에 따라 필드를 조작
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if not user.is_staff:
self.fields.pop('phone')
self.fields['username'].disabled = True
forms.Field 공통 속성(label, help_text, error_messages, initial, validators 등)과 Meta 속성(fields/exclude/widgets/labels 등)은 문서 표가 잘 정리돼 있어 생략합니다. Form vs ModelForm의 기준은 단순합니다 — 저장할 모델이 있으면 ModelForm, 검색·문의처럼 모델 없는 입력이면 Form.
3. crispy-forms의 Layout
crispy의 핵심은 레이아웃이 템플릿이 아니라 Python 코드에 있다는 것입니다.
self.helper.layout = Layout(
HTML("<h3 class='text-lg font-bold mb-4'>기본 정보</h3>"),
Row(Column('username'), Column('email')),
Row(Column('birth_date'), Column('phone')),
Submit('submit', '저장하기', css_class='btn btn-primary w-full'),
)
폼 구조가 바뀔 때 템플릿을 안 건드려도 되고, 조건부 레이아웃(__init__에서 layout 조립)이 자연스럽습니다.
4. 실제로 고른 조합 — 하나가 아니라 둘이었다
조사할 때는 표의 다섯 개가 경쟁 관계인 줄 알았습니다. 실제 프로젝트(표준데이터 관리 시스템)의 requirements를 보면 결말이 이렇습니다:
django-crispy-forms==2.3
crispy-bootstrap5==2024.10
django-widget-tweaks==1.5.0
crispy와 widget-tweaks를 같이 씁니다. 역할이 달라서예요 — 구조가 정형화된 폼(등록·수정 화면)은 crispy의 Layout으로 Python에서 조립하고, 템플릿에서 그때그때 클래스 하나 얹거나 속성 하나 바꾸는 미세 조정은 widget-tweaks({{ form.field | add_class:"..." }})로 합니다. 표의 "구조 제어 ✅ / ❌" 축은 우열이 아니라 서로 다른 층이었던 거죠. 비교표를 만들 때 "무엇을 고를까"만 묻고 "같이 쓸 수 있는가"를 안 물었던 게 이 조사의 빈틈이었습니다.
dynamic-formset은 탈락했습니다 — 첨부파일처럼 행을 추가/삭제하는 formset UI는 필요했지만, 라이브러리 대신 management_form의 TOTAL_FORMS를 직접 조작하는 jQuery 클론 패턴으로 갔어요. 의존성 하나 값을 못 하는 분량이라서요. 그 패턴은 1:1 Q&A 게시판 글의 첨부 절에, 서버 쪽 inline formset 구성은 별도 글에 있습니다.
정리
- 폼 레이아웃 도구는 "코드에서 구조를 짤 것인가(crispy)"와 "템플릿에서 조정할 것인가(widget-tweaks)"의 두 층이고, 실무 답은 층별로 하나씩이었습니다.
- 비교표에는 "함께 쓸 수 있는가" 열이 필요했습니다. 경쟁 구도로만 보면 보완 관계를 놓칩니다.